/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this sample’s licensing information

Abstract:
Part of Core Audio AUBase Classes
*/

#include "AUScopeElement.h"
#include <AudioUnit/AudioUnitProperties.h>
#include "AUBase.h"

/* This is defined in later MacOSX SDKs than the SDK used on the official build systems */
#ifndef kAudioUnitParameterFlag_OmitFromPresets
 #define kAudioUnitParameterFlag_OmitFromPresets (1UL << 13)
#endif

//_____________________________________________________________________________
//
//  By default, parameterIDs may be arbitrarily spaced, and an STL map
//  will be used for access.  Calling UseIndexedParameters() will
//  instead use an STL vector for faster indexed access.
//  This assumes the paramIDs are numbered 0.....inNumberOfParameters-1
//  Call this before defining/adding any parameters with SetParameter()
//
void    AUElement::UseIndexedParameters(int inNumberOfParameters)
{
    mIndexedParameters.resize (inNumberOfParameters);
    mUseIndexedParameters = true;
}

//_____________________________________________________________________________
//
//  Helper method.
//  returns the ParameterMapEvent object associated with the paramID
//
inline ParameterMapEvent&   AUElement::GetParamEvent(AudioUnitParameterID paramID)
{
    ParameterMapEvent *event;

    if(mUseIndexedParameters)
    {
        if(paramID >= mIndexedParameters.size() )
            COMPONENT_THROW(kAudioUnitErr_InvalidParameter);

        event = &mIndexedParameters[paramID];
    }
    else
    {
        ParameterMap::iterator i = mParameters.find(paramID);
        if (i == mParameters.end())
            COMPONENT_THROW(kAudioUnitErr_InvalidParameter);

        event = &(*i).second;
    }

    return *event;
}

//_____________________________________________________________________________
//
//  Helper method.
//  returns whether the specified paramID is known to the element
//
bool        AUElement::HasParameterID (AudioUnitParameterID paramID) const
{
    if(mUseIndexedParameters)
    {
        if(paramID >= mIndexedParameters.size() )
            return false;

        return true;
    }

    ParameterMap::const_iterator i = mParameters.find(paramID);
    if (i == mParameters.end())
        return false;

    return true;
}

//_____________________________________________________________________________
//
//  caller assumes that this is actually an immediate parameter
//
AudioUnitParameterValue     AUElement::GetParameter(AudioUnitParameterID paramID)
{
    ParameterMapEvent &event = GetParamEvent(paramID);

    return event.GetValue();
}


//_____________________________________________________________________________
//
void            AUElement::GetRampSliceStartEnd(    AudioUnitParameterID        paramID,
                                                    AudioUnitParameterValue &   outStartValue,
                                                    AudioUnitParameterValue &   outEndValue,
                                                    AudioUnitParameterValue &   outValuePerFrameDelta )

{
    ParameterMapEvent &event = GetParamEvent(paramID);

    // works even if the value is constant (immediate parameter value)
    event.GetRampSliceStartEnd(outStartValue, outEndValue, outValuePerFrameDelta );
}

//_____________________________________________________________________________
//
AudioUnitParameterValue         AUElement::GetEndValue( AudioUnitParameterID        paramID)

{
    ParameterMapEvent &event = GetParamEvent(paramID);

    // works even if the value is constant (immediate parameter value)
    return event.GetEndValue();
}

//_____________________________________________________________________________
//
void            AUElement::SetParameter(AudioUnitParameterID paramID, AudioUnitParameterValue inValue, bool okWhenInitialized)
{
    if(mUseIndexedParameters)
    {
        ParameterMapEvent &event = GetParamEvent(paramID);
        event.SetValue(inValue);
    }
    else
    {
        ParameterMap::iterator i = mParameters.find(paramID);

        if (i == mParameters.end())
        {
            if (mAudioUnit->IsInitialized() && !okWhenInitialized) {
                // The AU should not be creating new parameters once initialized.
                // If a client tries to set an undefined parameter, we could throw as follows,
                // but this might cause a regression. So it is better to just fail silently.
                // COMPONENT_THROW(kAudioUnitErr_InvalidParameter);
#if DEBUG
                fprintf(stderr, "WARNING: %s SetParameter for undefined param ID %d while initialized. Ignoring..\n",
                                mAudioUnit->GetLoggingString(), (int)paramID);
#endif
            } else {
                // create new entry in map for the paramID (only happens first time)
                ParameterMapEvent event(inValue);
                mParameters[paramID] = event;
            }
        }
        else
        {
            // paramID already exists in map so simply change its value
            ParameterMapEvent &event = (*i).second;
            event.SetValue(inValue);
        }
    }
}

//_____________________________________________________________________________
//
void            AUElement::SetScheduledEvent(   AudioUnitParameterID            paramID,
                                                const AudioUnitParameterEvent   &inEvent,
                                                UInt32                          inSliceOffsetInBuffer,
                                                UInt32                          inSliceDurationFrames,
                                                bool                            okWhenInitialized )
{
    if(mUseIndexedParameters)
    {
        ParameterMapEvent &event = GetParamEvent(paramID);
        event.SetScheduledEvent(inEvent, inSliceOffsetInBuffer, inSliceDurationFrames );
    }
    else
    {
        ParameterMap::iterator i = mParameters.find(paramID);

        if (i == mParameters.end())
        {
            if (mAudioUnit->IsInitialized() && !okWhenInitialized) {
                // The AU should not be creating new parameters once initialized.
                // If a client tries to set an undefined parameter, we could throw as follows,
                // but this might cause a regression. So it is better to just fail silently.
                // COMPONENT_THROW(kAudioUnitErr_InvalidParameter);
#if DEBUG
                fprintf(stderr, "WARNING: %s SetScheduledEvent for undefined param ID %d while initialized. Ignoring..\n",
                                mAudioUnit->GetLoggingString(), (int)paramID);
#endif
            } else {
                // create new entry in map for the paramID (only happens first time)
                ParameterMapEvent event(inEvent, inSliceOffsetInBuffer, inSliceDurationFrames);
                mParameters[paramID] = event;
            }
        }
        else
        {
            // paramID already exists in map so simply change its value
            ParameterMapEvent &event = (*i).second;

            event.SetScheduledEvent(inEvent, inSliceOffsetInBuffer, inSliceDurationFrames );
        }
    }
}



//_____________________________________________________________________________
//
void            AUElement::GetParameterList(AudioUnitParameterID *outList)
{
    if(mUseIndexedParameters)
    {
        UInt32 nparams = static_cast<UInt32>(mIndexedParameters.size());
        for (UInt32 i = 0; i < nparams; i++ )
            *outList++ = (AudioUnitParameterID)i;
    }
    else
    {
        for (ParameterMap::iterator i = mParameters.begin(); i != mParameters.end(); ++i)
            *outList++ = (*i).first;
    }
}

//_____________________________________________________________________________
//
void            AUElement::SaveState(AudioUnitScope scope, CFMutableDataRef data)
{
    AudioUnitParameterInfo paramInfo;
    CFIndex countOffset = CFDataGetLength(data);
    UInt32 nparams, nOmitted = 0, theData;

    if(mUseIndexedParameters)
    {
        nparams = static_cast<UInt32>(mIndexedParameters.size());
        theData = CFSwapInt32HostToBig(nparams);
        CFDataAppendBytes(data, (UInt8 *)&theData, sizeof(nparams));

        for (UInt32 i = 0; i < nparams; i++)
        {
            struct {
                UInt32              paramID;
                //CFSwappedFloat32  value; crashes gcc3 PFE
                UInt32              value;  // really a big-endian float
            } entry;

            if (mAudioUnit->GetParameterInfo(scope, i, paramInfo) == noErr) {
                if ((paramInfo.flags & kAudioUnitParameterFlag_CFNameRelease) && paramInfo.cfNameString)
                    CFRelease(paramInfo.cfNameString);
                if (paramInfo.flags & kAudioUnitParameterFlag_OmitFromPresets) {
                    ++nOmitted;
                    continue;
                }
            }

            entry.paramID = CFSwapInt32HostToBig(i);

            AudioUnitParameterValue v = mIndexedParameters[i].GetValue();
            entry.value = CFSwapInt32HostToBig(*(UInt32 *)&v );

            CFDataAppendBytes(data, (UInt8 *)&entry, sizeof(entry));
        }
    }
    else
    {
        nparams = static_cast<uint32_t>(mParameters.size());
        theData = CFSwapInt32HostToBig(nparams);
        CFDataAppendBytes(data, (UInt8 *)&theData, sizeof(nparams));

        for (ParameterMap::iterator i = mParameters.begin(); i != mParameters.end(); ++i) {
            struct {
                UInt32              paramID;
                //CFSwappedFloat32  value; crashes gcc3 PFE
                UInt32              value;  // really a big-endian float
            } entry;

            if (mAudioUnit->GetParameterInfo(scope, (*i).first, paramInfo) == noErr) {
                if ((paramInfo.flags & kAudioUnitParameterFlag_CFNameRelease) && paramInfo.cfNameString)
                    CFRelease(paramInfo.cfNameString);
                if (paramInfo.flags & kAudioUnitParameterFlag_OmitFromPresets) {
                    ++nOmitted;
                    continue;
                }
            }

            entry.paramID = CFSwapInt32HostToBig((*i).first);

            AudioUnitParameterValue v = (*i).second.GetValue();
            entry.value = CFSwapInt32HostToBig(*(UInt32 *)&v );

            CFDataAppendBytes(data, (UInt8 *)&entry, sizeof(entry));
        }
    }
    if (nOmitted > 0) {
        theData = CFSwapInt32HostToBig(nparams - nOmitted);
        *(UInt32 *)(CFDataGetBytePtr(data) + countOffset) = theData;
    }
}

//_____________________________________________________________________________
//
const UInt8 *   AUElement::RestoreState(const UInt8 *state)
{
    union FloatInt32 { UInt32 i; AudioUnitParameterValue f; };
    const UInt8 *p = state;
    UInt32 nparams = CFSwapInt32BigToHost(*(UInt32 *)p);
    p += sizeof(UInt32);

    for (UInt32 i = 0; i < nparams; ++i) {
        struct {
            AudioUnitParameterID        paramID;
            AudioUnitParameterValue     value;
        } entry;

        entry.paramID = CFSwapInt32BigToHost(*(UInt32 *)p);
        p += sizeof(UInt32);
        FloatInt32 temp;
        temp.i = CFSwapInt32BigToHost(*(UInt32 *)p);
        entry.value = temp.f;
        p += sizeof(AudioUnitParameterValue);

        SetParameter(entry.paramID, entry.value);
    }
    return p;
}

//_____________________________________________________________________________
//
void    AUElement::SetName (CFStringRef inName)
{
    if (mElementName) CFRelease (mElementName);
    mElementName = inName;
    if (mElementName) CFRetain (mElementName);
}


//_____________________________________________________________________________
//
AUIOElement::AUIOElement(AUBase *audioUnit) :
    AUElement(audioUnit),
    mWillAllocate (true)
{
    mStreamFormat = CAStreamBasicDescription(kAUDefaultSampleRate, 2, CAStreamBasicDescription::kPCMFormatFloat32, audioUnit->AudioUnitAPIVersion() == 1);
        // stereo
        // interleaved if API version 1, deinterleaved if version 2
}

//_____________________________________________________________________________
//
OSStatus        AUIOElement::SetStreamFormat(const CAStreamBasicDescription &desc)
{
    mStreamFormat = desc;
    return AUBase::noErr;
}

//_____________________________________________________________________________
// inFramesToAllocate == 0 implies the AudioUnit's max-frames-per-slice will be used
void            AUIOElement::AllocateBuffer(UInt32 inFramesToAllocate)
{
    if (GetAudioUnit()->HasBegunInitializing())
    {
        UInt32 framesToAllocate = inFramesToAllocate > 0 ? inFramesToAllocate : GetAudioUnit()->GetMaxFramesPerSlice();

//      printf ("will allocate: %d\n", (int)((mWillAllocate && NeedsBufferSpace()) ? framesToAllocate : 0));

        mIOBuffer.Allocate(mStreamFormat, (mWillAllocate && NeedsBufferSpace()) ? framesToAllocate : 0);
    }
}

//_____________________________________________________________________________
//
void            AUIOElement::DeallocateBuffer()
{
    mIOBuffer.Deallocate();
}

//_____________________________________________________________________________
//
//      AudioChannelLayout support

// outLayoutTagsPtr WILL be NULL if called to find out how many
// layouts that Audio Unit will report
// return 0 (ie. NO channel layouts) if the AU doesn't require channel layout knowledge
UInt32      AUIOElement::GetChannelLayoutTags (AudioChannelLayoutTag        *outLayoutTagsPtr)
{
    return 0;
}

// As the AudioChannelLayout can be a variable length structure
// (though in most cases it won't be!!!)
// The size of the ACL is always returned by the method
// if outMapPtr is NOT-NULL, then AU should copy into this pointer (outMapPtr) the current ACL that it has in use.
// the AU should also return whether the property is writable (that is the client can provide any arbitrary ACL that the audio unit will then honour)
// or if the property is read only - which is the generally preferred mode.
// If the AU doesn't require an AudioChannelLayout, then just return 0.
UInt32      AUIOElement::GetAudioChannelLayout (AudioChannelLayout  *outMapPtr,
                                                Boolean             &outWritable)
{
    return 0;
}

// the incoming channel map will be at least as big as a basic AudioChannelLayout
// but its contents will determine its actual size
// Subclass should overide if channel map is writable
OSStatus    AUIOElement::SetAudioChannelLayout (const AudioChannelLayout &inData)
{
    return kAudioUnitErr_InvalidProperty;
}

// Some units support optional usage of channel maps - typically converter units
// that can do channel remapping between different maps. In that optional case
// the user should be able to remove a channel map if that is possible.
// Typically this is NOT the case (e.g., the 3DMixer even in the stereo case
// needs to know if it is rendering to speakers or headphones)
OSStatus    AUIOElement::RemoveAudioChannelLayout ()
{
    return kAudioUnitErr_InvalidPropertyValue;
}


//_____________________________________________________________________________
//
AUScope::~AUScope()
{
    for (ElementVector::iterator it = mElements.begin(); it != mElements.end(); ++it)
        delete *it;
}

//_____________________________________________________________________________
//
void    AUScope::SetNumberOfElements(UInt32 numElements)
{
    if (mDelegate)
        return mDelegate->SetNumberOfElements(numElements);

    if (numElements > mElements.size()) {
        mElements.reserve(numElements);
        while (numElements > mElements.size()) {
            AUElement *elem = mCreator->CreateElement(GetScope(), static_cast<UInt32>(mElements.size()));
            mElements.push_back(elem);
        }
    } else
        while (numElements < mElements.size()) {
            AUElement *elem = mElements.back();
            mElements.pop_back();
            delete elem;
        }
}

//_____________________________________________________________________________
//
bool    AUScope::HasElementWithName () const
{
    for (UInt32 i = 0; i < GetNumberOfElements(); ++i) {
        AUElement * el = const_cast<AUScope*>(this)->GetElement (i);
        if (el && el->HasName()) {
            return true;
        }
    }
    return false;
}

//_____________________________________________________________________________
//

void    AUScope::AddElementNamesToDict (CFMutableDictionaryRef & inNameDict)
{
    if (HasElementWithName())
    {
        static char string[32];
        CFMutableDictionaryRef elementDict = CFDictionaryCreateMutable  (NULL, 0,
                                &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        CFStringRef str;
        for (UInt32 i = 0; i < GetNumberOfElements(); ++i) {
            AUElement * el = GetElement (i);
            if (el && el->HasName()) {
                snprintf (string, sizeof(string), "%d", int(i));
                str = CFStringCreateWithCString (NULL, string, kCFStringEncodingASCII);
                CFDictionarySetValue (elementDict, str, el->GetName());
                CFRelease (str);
            }
        }

        snprintf (string, sizeof(string), "%d", int(mScope));
        str = CFStringCreateWithCString (NULL, string, kCFStringEncodingASCII);
        CFDictionarySetValue (inNameDict, str, elementDict);
        CFRelease (str);
        CFRelease (elementDict);
    }
}

//_____________________________________________________________________________
//
bool    AUScope::RestoreElementNames (CFDictionaryRef& inNameDict)
{
    static char string[32];

    //first we have to see if we have enough elements
    bool didAddElements = false;
    unsigned int maxElNum = GetNumberOfElements();

    int dictSize = static_cast<int>(CFDictionaryGetCount(inNameDict));
    CFStringRef * keys = (CFStringRef*)CA_malloc (dictSize * sizeof (CFStringRef));
    CFDictionaryGetKeysAndValues (inNameDict, reinterpret_cast<const void**>(keys), NULL);
    for (int i = 0; i < dictSize; i++)
    {
        unsigned int intKey = 0;
        CFStringGetCString (keys[i], string, 32, kCFStringEncodingASCII);
        int result = sscanf (string, "%u", &intKey);
        // check if sscanf succeeded and element index is less than max elements.
        if (result && UInt32(intKey) < maxElNum)
        {
            CFStringRef elName = reinterpret_cast<CFStringRef>(CFDictionaryGetValue (inNameDict,  keys[i]));
            AUElement* element = GetElement (intKey);
            if (element)
                element->SetName (elName);
        }
    }
    free (keys);

    return didAddElements;
}

void    AUScope::SaveState(CFMutableDataRef data)
{
    AudioUnitElement nElems = GetNumberOfElements();
    for (AudioUnitElement ielem = 0; ielem < nElems; ++ielem) {
        AUElement *element = GetElement(ielem);
        UInt32 nparams = element->GetNumberOfParameters();
        if (nparams > 0) {
            struct {
                UInt32  scope;
                UInt32  element;
            } hdr;

            hdr.scope = CFSwapInt32HostToBig(GetScope());
            hdr.element = CFSwapInt32HostToBig(ielem);
            CFDataAppendBytes(data, (UInt8 *)&hdr, sizeof(hdr));

            element->SaveState(mScope, data);
        }
    }
}

const UInt8 *   AUScope::RestoreState(const UInt8 *state)
{
    const UInt8 *p = state;
    UInt32 elementIdx = CFSwapInt32BigToHost(*(UInt32 *)p); p += sizeof(UInt32);
    AUElement *element = GetElement(elementIdx);
    if (!element) {
        struct {
            AudioUnitParameterID        paramID;
            AudioUnitParameterValue     value;
        } entry;
        UInt32 nparams = CFSwapInt32BigToHost(*(UInt32 *)p);
        p += sizeof(UInt32);

        p += nparams * sizeof(entry);
    } else
        p = element->RestoreState(p);

    return p;
}
